using System;
using System.Net;
using UnityEngine;
using System.Collections.Generic;
using Fusion.Utilities.Bit;

namespace Fusion.Net.UDP
{
    public class DataCache
    {
        private byte[][] datas;

        private int seq = 0;
        private int maxSeq = 0;

        private int length = 0;

        private EndPoint ip;

        private int pullNullCount = 0;
        private int pullNullLimit = -1;

        public EndPoint IP { get => ip; }
        public int PullNullLimit { get => pullNullLimit; }
        public int MaxSeq { 
            get 
            { 
                lock (this)
                {
                    return maxSeq; 
                }
            } 
        }

        public int Seq 
        {
            get
            {
                lock (this)
                {
                    return seq;
                }
            }
        }

        public DataCache(EndPoint ip) : this(ip, -1)
        {
        }

        public DataCache(EndPoint ip, int pullNullLimit)
        {
            this.ip = ip;
            length = NetConfig.Data_Cache_Size;
            datas = new byte[length][];
            this.pullNullLimit = pullNullLimit;
        }

        public void Reset()
        {
            lock (this)
            {
                seq = 0;
                maxSeq = 0;
            }
        }

        public bool CheckSeqEmpty(int seq)
        {
            lock (this)
            {
                return datas[GetIndex(seq)] == null;
            }
        }

        public bool CheckFull()
        {
            lock (this)
            {
                int totalRemain = int.MaxValue - maxSeq;
                if (totalRemain >= length)
                {
                    return seq + length <= maxSeq;
                }
                else
                {
                    return totalRemain <= 0;
                }
            }
        }

        public int GetFreeNum()
        {
            lock (this)
            {
                int totalRemain = int.MaxValue - maxSeq;
                if (totalRemain >= length)
                {
                    return seq + length - maxSeq;
                }
                else
                {
                    return totalRemain;
                }
            }
        }

        private int GetIndex(int seq)
        {
            return seq % length;
        }

        public void Set(int seq, byte[] data)
        {
            lock (this)
            {
                int totalRemain = int.MaxValue - this.seq;
                int length = totalRemain > this.length ? this.length : totalRemain;
                if (seq >= this.seq && seq < (this.seq + length) && CheckSeqEmpty(seq))
                {
                    datas[GetIndex(seq)] = data;

                    if (seq >= maxSeq)
                    {
                        maxSeq = seq + 1;
                    }
                }
            }
        }
        List<byte[]> pushResult = new List<byte[]>();

        public List<byte[]> Push(int seq, byte[] data)
        {
            lock (this)
            {
                if (seq >= this.seq && CheckSeqEmpty(seq))
                {
                    if (seq < (this.seq + length))
                    {
                        datas[GetIndex(seq)] = data;
                    }
                    else
                    {
                        pushResult.Clear();
                        pullNullCount = 0;
                        int newSeq = seq - length + 1;
                        for (int i = this.seq; i < newSeq; i++)
                        {
                            int index = GetIndex(i);
                            if (datas[index] != null)
                            {
                                pushResult.Add(datas[index]);
                                datas[index] = null;
                            }
                        }

                        datas[GetIndex(seq)] = data;
                    }

                    if (seq >= maxSeq)
                    {
                        maxSeq = seq + 1;
                    }
                }

                return pushResult;
            }
        }
        public int Pull(out byte[] result) {
            lock (this)
            {
                if (!CheckSeqEmpty(seq))
                {
                    int index = GetIndex(seq);
                    pullNullCount = 0;
                    result = datas[index];
                    datas[index] = null;
                    seq++;
                    return 0;
                }
                else
                {
                    result = null;
                    if (pullNullLimit > 0 && maxSeq > seq)
                    {
                        pullNullCount++;
                        if (pullNullCount >= pullNullLimit)
                        {
                            pullNullCount = 0;
                            seq++;
                        }
                    }
                    return 0;
                }
            }
        }

        byte[] recvedResult = null;
        int recvedDataType = 0;

        int remainSize = 0;
        public int Get(out byte[] result, int bufferSize, out int dataType)
        {
            lock (this)
            {
                if (CheckSeqEmpty(seq))
                {
                    result = null;
                    dataType = -1;
                    return 0;
                }

                int index = GetIndex(seq);
                int recvCount = maxSeq - seq;
                byte[] data = datas[index];

                if (recvedResult != null)
                {
                    int remainCount = Mathf.CeilToInt((float)remainSize / bufferSize);
                    bool isRemainDataCompleted = true;

                    if (remainCount > recvCount)
                    {
                        isRemainDataCompleted = false;
                    }
                    else
                    {
                        int maxRecvSeq = seq + remainCount;
                        for (int i = seq; i < maxRecvSeq; i++)
                        {
                            if (CheckSeqEmpty(i))
                            {
                                isRemainDataCompleted = false;
                                break;
                            }
                        }
                    }

                    if (isRemainDataCompleted)
                    {
                        int offset = recvedResult.Length - remainSize;
                        int maxRecvSeq = seq + remainCount;
                        int singleDataLength = data.Length - 5;

                        //UnityEngine.Debug.LogError("R " + seq);

                        Array.Copy(data, 5, recvedResult, offset, singleDataLength);
                        offset += singleDataLength;
                        datas[index] = null;

                        for (int i = seq + 1; i < maxRecvSeq; i++)
                        {
                        //UnityEngine.Debug.LogError("R " + i);

                            int subIndex = GetIndex(i);
                            data = datas[subIndex];
                            datas[subIndex] = null;
                            singleDataLength = data.Length - 5;
                            Array.Copy(data, 5 , recvedResult, offset, singleDataLength);
                            offset += singleDataLength;
                        }
                        seq = maxRecvSeq;
                        result = recvedResult;
                        recvedResult = null;
                        dataType = recvedDataType;
                        return offset;
                    }
                    else
                    {
                        result = null;
                        dataType = -1;
                        return 0;
                    }
                }


                int length = BitUtility.ReadInt(data, 5);
                int count = Mathf.CeilToInt((float)length / bufferSize);
                bool isDataCompleted = true;


                if (count > recvCount && maxSeq != int.MaxValue)
                {
                    isDataCompleted = false;
                }
                else
                {
                    count = count > recvCount ? recvCount : count;
                    int maxRecvSeq = seq + count;
                    for (int i = seq; i < maxRecvSeq; i++)
                    {
                        if (CheckSeqEmpty(i))
                        {
                            isDataCompleted = false;
                            break;
                        }
                    }
                }

                if (isDataCompleted)
                {
                        //UnityEngine.Debug.LogError("N " + seq);

                    int maxRecvSeq = seq + count;
                    result = new byte[length];
                    dataType = data[9];
                    int singleDataLength = data.Length - 10;
                    int offset = singleDataLength;
                    Array.Copy(data, 10, result, 0, singleDataLength);
                    datas[index] = null;
                    for (int i = seq + 1; i < maxRecvSeq; i++)
                    {
                        //UnityEngine.Debug.LogError("N " + i);
                        int subIndex = GetIndex(i);
                        data = datas[subIndex];
                        datas[subIndex] = null;
                        singleDataLength = data.Length - 5;
                        Array.Copy(data, 5, result, offset, singleDataLength);
                        offset += singleDataLength;
                    }
                    seq = maxRecvSeq;
                    if (offset < length)
                    {
                        recvedResult = result;
                        remainSize = length - offset;
                        recvedDataType = dataType;
                    }

                    return offset;
                }
                else
                {
                    result = null;
                    dataType = -1;
                    return 0;
                }
            }
        }

        public bool Remove(int seq)
        {
            lock (this)
            {
                if (seq >= this.seq && seq < maxSeq)
                {
                    int index = GetIndex(seq);

                    datas[index] = null;

                    if (seq == this.seq)
                    {
                        int nextSeq = maxSeq;
                        for (int i = seq + 1; i < maxSeq; i++)
                        {
                            int nextIndex = GetIndex(i);
                            if (datas[nextIndex] != null)
                            {
                                nextSeq = i;
                                break;
                            }
                        }

                        this.seq = nextSeq;
                    }
                    return true;
                }
                else
                {
                    return false;
                }
            }
        }

        public byte[] Get(int seq)
        {
            lock (this)
            {
                if (seq >= this.seq && seq < maxSeq)
                {
                    int index = GetIndex(seq);
                    return datas[index];
                }
                else
                {
                    return null;
                }
            }
        }
    }

}
